import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Set;

import javax.swing.JPanel;

public class QuiverPanel extends JPanel implements MouseListener,
		MouseMotionListener {
	private static final long serialVersionUID = 1L;
	public final Configuration config = Configuration.getDefaultConfiguration();
	public Quiver quiver;
	private Arrow indicatedArrow;
	private Node indicatedNode;
	private Node heldNode, selectedNode, hoveredNode;
	private boolean nodeMoved, arrowMoved;
	private Arrow selectedArrow;
	private LabelInput labelInput;
	private double oldArrowX, oldArrowY;
	private Arrow heldArrow;
	private ArrayList<Quiver> history = new ArrayList<Quiver>();
	private int historyEntry = -1;
	private ArrayList<Representation> representations = new ArrayList<Representation>();
	private Representation activeRepr = null;

	public QuiverPanel(Quiver quiver) {
		setLayout(null);

		this.quiver = quiver;

		setBackground(config.backgroundColor);
		setFocusable(true);

		addMouseListener(this);
		addMouseMotionListener(this);

		addEvent();
	}

	public void addEvent() {
		if (config.undoSize <= 0) {
			return;
		}
		int size = history.size();
		for (int i = historyEntry + 1; i < size; i++) {
			history.remove(history.size() - 1);
		}
		if (history.size() == config.undoSize) {
			history.remove(0);
			historyEntry--;
		}
		history.add((Quiver) quiver.Clone());
		historyEntry++;
		Actions.undoAction.setEnabled(canUndo());
		Actions.redoAction.setEnabled(canRedo());
	}

	public void undo() {
		if (historyEntry > 0) {
			historyEntry--;
			quiver = (Quiver) history.get(historyEntry).Clone();
			repaint();
			Actions.undoAction.setEnabled(canUndo());
			Actions.redoAction.setEnabled(canRedo());
		}
	}

	public void redo() {
		if (historyEntry < history.size() - 1) {
			historyEntry++;
			quiver = (Quiver) history.get(historyEntry).Clone();
			repaint();
			Actions.undoAction.setEnabled(canUndo());
			Actions.redoAction.setEnabled(canRedo());
		}
	}

	public boolean canUndo() {
		return (historyEntry > 0);
	}

	public boolean canRedo() {
		return (historyEntry < history.size() - 1);
	}

	public void move() {
		Set<Node> nodes = quiver.getNodes();
		int numNodes = nodes.size();
		for (Node node1 : nodes) {
			if (node1 == heldNode) {
				continue;
			}
			double fx = 0, fy = 0;
			for (Node node2 : nodes) {
				if (node1 == node2) {
					continue;
				}
				// should preferred distance increase with number of nodes?
				double force = -0.5
						* config.hookesConstant
						* (config.preferredNodeDistance + 5 * numNodes - node1
								.distance(node2));
				double angle = node1.getAngle(node2);
				fx += force * Math.cos(angle);
				fy += force * Math.sin(angle);
			}
			Point2D point = node1.getPoint();
			node1.move(point.getX() + fx, point.getY() + fy);
		}
		repaint();
	}

	public void paintGrid(Graphics2D g) {
		double width = getWidth();
		double height = getHeight();
		g.setPaint(config.gridColor);
		for (double x = 0; x < width; x += config.gridSize) {
			g.drawLine((int) x, 0, (int) x, (int) height);
		}
		for (double y = 0; y < height; y += config.gridSize) {
			g.drawLine(0, (int) y, (int) width, (int) y);
		}
	}

	public void paint(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;
		super.paint(g2);
		if (config.gridVisible) {
			paintGrid(g2);
		}
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		for (Node node : quiver.getNodes()) {
			if (activeRepr != null) {
				node.paint(g2, activeRepr);
			} else {
				node.paint(g2);
			}
		}
		for (Arrow arrow : quiver.getArrows()) {
			if (arrow.getFromNode() == arrow.getToNode()
					|| !arrow.getFromNode().isClose(arrow.getToNode())) {
				if (activeRepr != null) {
					arrow.paint(g2, activeRepr);
				} else {
					arrow.paint(g2);
				}
			}
		}
		if (indicatedArrow != null && selectedNode != null
				&& !indicatedNode.isClose(selectedNode)) {
			indicatedArrow.paint(g2);
			indicatedNode.paint(g2);
		}
		if (labelInput != null){
			labelInput.repaint();
		}
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		selectLabel(null);
		Node node = quiver.getNode(e.getPoint());
		Named label = quiver.getLabel(e.getPoint());
		Arrow arrow = quiver.getArrow(e.getPoint());
		switch (e.getButton()) {
		case MouseEvent.BUTTON1:
			if (selectedNode == null) {
				if (node != null) {
					selectNode(node);
					indicateNode(node);
				} else if (arrow != null && e.isControlDown()) {
					arrow.flip();
					repaint();
					addEvent();
				} else if (label != null) {
					selectLabel(label);
				} else if (arrow != null) {
					selectArrow(arrow);
				} else {
					quiver.addNode(e.getX(), e.getY());
					repaint();
					selectArrow(null);
					addEvent();
				}
			} else {
				if (node != null) {
					quiver.addArrow(selectedNode, node);
					addEvent();
				} else if (label != null) {
					selectLabel(label);
				} else {
					Node newNode = quiver.addNode(e.getX(), e.getY());
					selectArrow(quiver.addArrow(selectedNode, newNode));
					addEvent();
				}
				selectNode(null);
			}
			break;
		case MouseEvent.BUTTON3:
			if (heldNode != null) {
				heldNode.cancelMove();
				holdNode(null);
			} else if (node != null) {
				quiver.removeNode(node);
				addEvent();
			} else if (arrow != null) {
				quiver.removeArrow(arrow);
				addEvent();
				repaint();
			}
			if (selectedArrow != null) {
				selectArrow(null);
			}
			if (selectedNode != null) {
				selectNode(null);
			}
			selectLabel(null);
		}
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mousePressed(MouseEvent e) {
		selectLabel(null);
		switch (e.getButton()) {
		case MouseEvent.BUTTON1:
			if (heldNode == null) {
				Node node = quiver.getNode(e.getPoint());
				if (node != null) {
					holdNode(node);
				}
			}
			if (heldArrow == null && heldNode == null) {
				Arrow arrow = quiver.getArrow(e.getPoint());
				if (arrow != null) {
					holdArrow(arrow);
				}
				oldArrowX = e.getX();
				oldArrowY = e.getY();
			}
			break;
		}
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		switch (e.getButton()) {
		case MouseEvent.BUTTON1:
			if (heldNode != null) {
				if (nodeMoved) {
					Node node = quiver.getNode(e.getPoint(), heldNode);
					if (node != null) {
						quiver.copyArrows(heldNode, node);
						quiver.removeNode(heldNode);
					}
					addEvent();
				}
				holdNode(null);
			}
			if (heldArrow != null) {
				if (arrowMoved) {
					addEvent();
				}
				holdArrow(null);
			}
			break;
		}
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		if (heldNode != null && (nodeMoved || !heldNode.isClose(e.getPoint()))) {
			Node node = quiver.getNode(e.getPoint(), heldNode);
			if (node != null) {
				heldNode.move(node.getPoint());
			} else {
				if (e.isShiftDown()) {
					heldNode.snapToGrid(e.getX(), e.getY());
				} else {
					heldNode.move(e.getX(), e.getY());
				}
			}
			nodeMoved = true;
			repaint();
		}
		if (heldArrow != null
				&& (arrowMoved || !heldArrow.isClose(e.getPoint()))) {
			heldArrow.move(e.getX() - oldArrowX, e.getY() - oldArrowY);
			oldArrowX = e.getX();
			oldArrowY = e.getY();
			arrowMoved = true;
			repaint();
		}
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		Node node = quiver.getNode(e.getPoint());
		if (node != null) {
			hoverNode(node);
		} else if (hoveredNode != null) {
			hoverNode(null);
		}
		if (indicatedNode != null && indicatedArrow != null) {
			if (hoveredNode != null && indicatedNode.isClose(hoveredNode)) {
				indicatedNode.move(hoveredNode.getPoint());
			} else {
				indicatedNode.move(e.getPoint());
			}
			indicatedArrow.update();
			repaint();
		}
	}

	public void selectArrow(Arrow arrow) {
		if (arrow == selectedArrow) {
			return;
		}
		if (selectedArrow != null) {
			selectedArrow.setSelected(false);
			selectedArrow = null;
			repaint();
		}
		if (arrow != null) {
			selectedArrow = arrow;
			selectedArrow.setSelected(true);
			repaint();
		}
	}

	public void indicateNode(Node node) {
		if (node == indicatedNode) {
			return;
		}
		if (node != null) {
			indicatedNode = new Node(node.getPoint().getX(), node.getPoint()
					.getY());
			indicatedNode.setIndicated(true);
			indicatedArrow = new Arrow(selectedNode, indicatedNode);
		} else if (selectedNode != null) {
			indicatedNode = null;
			indicatedArrow = null;
		}
	}

	public void selectNode(Node node) {
		if (node == selectedNode) {
			return;
		}
		if (node != null) {
			selectedNode = node;
			selectedNode.setSelected(true);
			repaint();
		} else if (selectedNode != null) {
			selectedNode.setSelected(false);
			selectedNode = null;
			repaint();
		}
	}

	public void selectLabel(Named object) {
		if (labelInput != null){
			labelInput.write();
			remove(labelInput);
		}
		if (object == null) {
			labelInput = null;
			return;
		}
		if (object instanceof Arrow && activeRepr != null) {
			labelInput = new MatrixInput(activeRepr, (Arrow) object);
		} else if (object instanceof Node && activeRepr != null) {
			labelInput = new DimensionInput(activeRepr, (Node) object);
		} else {
			labelInput = new NameInput(object);
		}
		add(labelInput);
	}
	
	public void holdNode(Node node) {
		if (node == heldNode) {
			return;
		}
		if (node != null) {
			heldNode = node;
			heldNode.setHeld(true);
			repaint();
		} else if (heldNode != null) {
			heldNode.setHeld(false);
			heldNode = null;
			nodeMoved = false;
			repaint();
		}
	}

	public void holdArrow(Arrow arrow) {
		if (arrow == heldArrow) {
			return;
		}
		if (arrow != null) {
			heldArrow = arrow;
			heldArrow.setHeld(true);
			repaint();
		} else if (heldArrow != null) {
			heldArrow.setHeld(false);
			heldArrow = null;
			arrowMoved = false;
			repaint();
		}
	}

	public void hoverNode(Node node) {
		if (node == hoveredNode) {
			return;
		}
		if (node != null) {
			hoveredNode = node;
			hoveredNode.setHovered(true);
			repaint();
		} else if (hoveredNode != null) {
			hoveredNode.setHovered(false);
			hoveredNode = null;
			repaint();
		}
	}

	public void clear() {
		quiver.clear();
		selectedNode = null;
		selectedArrow = null;
		hoveredNode = null;
		heldNode = null;
		heldArrow = null;
		indicatedNode = null;
		indicatedArrow = null;
		addEvent();
		repaint();
	}

	public void addRepresentation() {
		addRepresentation("");
	}

	public void addRepresentation(String name) {
		addRepresentation(new Representation(quiver));
	}

	public void addRepresentation(Representation representation) {
		representations.add(representation);
		repaint();
	}

	public Representation getActiveRepresentation() {
		return activeRepr;
	}

	public void setActiveRepresentation(Representation repr) {
		activeRepr = repr;
		repaint();
	}

	public ArrayList<Representation> getRepresentations() {
		return representations;
	}
}
